TravelService   A
last analyzed

Complexity

Total Complexity 22

Size/Duplication

Total Lines 169
Duplicated Lines 0 %

Test Coverage

Coverage 94.74%

Importance

Changes 0
Metric Value
eloc 135
dl 0
loc 169
ccs 54
cts 57
cp 0.9474
rs 10
c 0
b 0
f 0
wmc 22

9 Functions

Rating   Name   Duplication   Size   Complexity  
A findById 0 7 2
A findAll 0 3 1
A startRentingBike 0 19 2
A findActiveTravelForBike 0 15 2
A findTravelsForCustomer 0 3 1
A endActiveTravelForBike 0 4 1
A endAllTravelsForCustomer 0 24 3
A calculateCost 0 20 3
B endTravel 0 56 7
1 9
import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
2 9
import { InjectRepository } from '@nestjs/typeorm';
3 9
import { IsNull, Not, Repository } from 'typeorm';
4 9
import { Travel } from './entities/travel.entity';
5 9
import { BicyclesService } from '../bicycles/bicycles.service';
6 9
import { ZonesService } from 'src/zones/zones.service';
7
8
@Injectable()
9 9
export class TravelService {
10
  async findTravelsForCustomer(customerId: string) {
11 1
    return await this.travelRepository.find({
12
      where: { customer: { githubId: customerId } },
13
    });
14
  }
15
  constructor(
16
    @InjectRepository(Travel)
17 21
    private readonly travelRepository: Repository<Travel>,
18 21
    private readonly bicyclesService: BicyclesService,
19 21
    private readonly zonesService: ZonesService,
20
  ) {}
21
22
  async findAll(): Promise<Travel[]> {
23 2
    return await this.travelRepository.find();
24
  }
25
26
  async findById(id: number): Promise<Travel> {
27 3
    const travel = await this.travelRepository.findOne({ where: { id } });
28 3
    if (!travel) {
29 2
      throw new NotFoundException('Travel not found');
30
    }
31 1
    return travel;
32
  }
33
34
  async findActiveTravelForBike(bikeId: string): Promise<Travel> {
35 4
    const activeTravel = await this.travelRepository.findOne({
36
      where: {
37
        bike: { id: bikeId },
38
        startTime: Not(IsNull()),
39
        stopTime: IsNull(),
40
      },
41
    });
42
43 4
    if (!activeTravel) {
44 2
      throw new NotFoundException(`No active travel found for bike ${bikeId}`);
45
    }
46
47 2
    return activeTravel;
48
  }
49
50
  async startRentingBike(bikeId: string, customerId: string): Promise<Travel> {
51 28
    const bike = await this.bicyclesService.setRented(bikeId);
52 19
    const zoneType = this.zonesService.pointInParkingZone(bike.latitude, bike.longitude)
53
      ? 'Parking'
54
      : 'Free';
55
56 19
    const travel = this.travelRepository.create({
57
      bike,
58
      startTime: new Date(),
59
      latStart: bike.latitude,
60
      longStart: bike.longitude,
61
      customer: { githubId: customerId },
62
      startZoneType: zoneType,
63
      endZoneType: null,
64
      cost: 0,
65
    });
66
67 19
    return this.travelRepository.save(travel);
68
  }
69
70
  async endActiveTravelForBike(bikeId: string) {
71 2
    const activeTravel = await this.findActiveTravelForBike(bikeId);
72 1
    return this.endTravel(activeTravel.id);
73
  }
74
75
  async endTravel(travelId: number): Promise<Travel> {
76 5
    const travel = await this.travelRepository.findOne({
77
      where: { id: travelId },
78
      relations: ['bike', 'customer'], // Load bike and customer relations
79
    });
80
81 5
    if (!travel) {
82 1
      throw new NotFoundException('Travel not found');
83
    }
84
85 4
    if (travel.stopTime) {
86
      throw new BadRequestException('Travel has already ended');
87
    }
88
89
    // Get current bike location (from bike entity)
90 4
    const bike = await this.bicyclesService.findById(travel.bike.id);
91
92
    // Get the end zone type
93 4
    const endZones = await this.zonesService.getZoneTypesForPosition(bike.latitude, bike.longitude);
94
95
    // Set end time to current server time
96 4
    const endTime = new Date();
97
98
    // Calculate cost
99 4
    const cost = this.calculateCost(travel.startTime, endTime, travel.startZoneType, endZones);
100
101
    // Update travel record
102 4
    travel.stopTime = endTime;
103 4
    travel.latStop = bike.latitude;
104 4
    travel.longStop = bike.longitude;
105 4
    travel.endZoneType = endZones.includes('Parking') ? 'Parking' : 'Free';
106 4
    travel.cost = cost;
107
108 4
    const newStatus = endZones.includes('Charging') ? 'Service' : 'Available';
109 4
    await this.bicyclesService.update(bike.id, { status: newStatus });
110
111
    // Update user account
112 4
    const customer = travel.customer;
113
114 4
    if (customer.isMonthlyPayment) {
115
      // Accumulate cost for monthly payment users
116 4
      customer.accumulatedCost += cost;
117
    } else {
118
      // Deduct from balance for prepaid users
119 1
      if (customer.balance < cost) {
120
        throw new BadRequestException('Insufficient balance. Please insert funds.');
121
      }
122
      customer.balance -= cost;
123
    }
124
125
    // Save updated user
126 4
    await this.travelRepository.manager.getRepository('User').save(customer);
127
128
    // Save and return updated travel
129 4
    return this.travelRepository.save(travel);
130
  }
131
132
  async endAllTravelsForCustomer(githubId: string): Promise<string> {
133
    // Find all active travels for the customer
134 2
    const activeTravels = await this.travelRepository.find({
135
      where: {
136
        customer: { githubId: githubId },
137
        startTime: Not(IsNull()),
138
        stopTime: IsNull(),
139
      },
140
      relations: ['bike', 'customer'],
141
    });
142
143 2
    if (activeTravels.length === 0) {
144 1
      throw new NotFoundException(
145
        `No active travels found for customer with GitHub ID ${githubId}.`,
146
      );
147
    }
148
149
    // Loop through each active travel and end it using the existing endTravel method
150 1
    for (const travel of activeTravels) {
151 2
      await this.endTravel(travel.id);
152
    }
153
154 1
    return `All active travels for customer with GitHub ID ${githubId} have been successfully ended.`;
155
  }
156
157
  calculateCost(
158
    startTime: Date,
159
    endTime: Date,
160
    startZoneType: string,
161
    endZoneTypes: string[],
162
  ): number {
163 4
    const timeDiff = endTime.getTime() - startTime.getTime();
164 4
    const timeDiffInMinutes = timeDiff / 1000 / 60;
165
166 4
    const parkingFee = 10;
167 4
    const startFee = 10;
168 4
    const costPerMinute = 1;
169
170
    const cost =
171 4
      (endZoneTypes.includes('Parking') ? 0 : parkingFee) +
172
      (startZoneType === 'Free' && endZoneTypes.includes('Parking') ? startFee / 2 : startFee) +
173
      timeDiffInMinutes * costPerMinute;
174
175 4
    return cost;
176
  }
177
}
178